﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Windows;
using System.Windows.Data;

namespace JasLib.Wpf
{
    /*****************************************************************************/
    [Serializable]
    public class ViewModelBase
        : Freezable,
        INotifyPropertyChanging, INotifyPropertyChanged, IWeakEventListener,
        IEditableObject, IDataErrorInfo
    {
        /// <summary>
        /// This forces all classes to derive from the generic version.
        /// </summary>
        protected ViewModelBase()
        {
        }

        #region Freezable implementation

        protected override Freezable CreateInstanceCore()
        {
            return new ViewModelBase();
        }

        #endregion

        #region IWeakEventListener implementation

        public virtual bool ReceiveWeakEvent(Type managerType, object sender, EventArgs eventArgs)
        {
            if (managerType == typeof(PropertyChangedEventManager))
            {
                OnPropertyChanged((eventArgs as PropertyChangedEventArgs).PropertyName);
                return true;
            }

            return false;
        }

        #endregion

        #region IEditableObject implementation

        /// <summary>
        /// IEditableObject.BeginEdit() base implementation.
        /// </summary>
        public virtual void BeginEdit()
        {
        }

        /// <summary>
        /// IEditableObject.CancelEdit() base implementation.
        /// </summary>
        public virtual void CancelEdit()
        {
        }

        /// <summary>
        /// IEditableObject.EndEdit() base implementation.
        /// </summary>
        public virtual void EndEdit()
        {
        }

        #endregion

        #region IDataErrorInfo implementation

        /// <summary>
        /// IDataErrorInfo.Error base implementation. WPF does not use this.
        /// </summary>
        string IDataErrorInfo.Error
        {
            get { return null; }
        }

        /// <summary>
        /// IDataErrorInfo[string] base implementation.
        /// Derived classes may want to use string indexing for their own purposes instead of error strings,
        /// thus we use the more explicitly-named GetErrorString() virtual function.
        /// </summary>
        string IDataErrorInfo.this[string strPropertyName]
        {
            get { return GetErrorString(strPropertyName); }
        }

        /// <summary>
        /// Provides the return value for IDataErrorInfo[string].
        /// </summary>
        public virtual string GetErrorString(string strPropertyName)
        {
            return null;
        }

        #endregion

        /*
         * The downside to using generic forms of OnPropertyChanging and OnPropertyChanged is that
         * the JIT will build an identical copy for each value type of T.
         * But the alternative was to box everything as objects, which would put us right back where
         * we started with heap fragmentation (which the Property*EventArgs caches were supposed to alleviate).
         * What C# needs is the ability to only override the version of a virtual generic method for a specific type,
         * or otherwise to overload any generic method via constraint.
         */

        #region INotifyPropertyChanging implementation

        public event PropertyChangingEventHandler PropertyChanging;

        internal bool TrySendPropertyChangingEvent(string strPropertyName)
        {
            if (PropertyChanging != null)
            {
                var args = EventArgsCache.GetNotifyPropertyChangingEventArgs(strPropertyName);
                PropertyChanging(this, args);
                return true;
            }
            return false;
        }

        protected virtual void OnPropertyChanging(/*[CallerMemberName]*/ string strPropertyName)
        {
            TrySendPropertyChangingEvent(strPropertyName);
            return;
        }

        protected void OnPropertyChanging<TProperty>(Expression<Func<TProperty>> propertySelector)
        {
            OnPropertyChanging(WpfUtilities.GeneratePropertyPathString(propertySelector));
            return;
        }

        protected void OnPropertyChanging<TDerived, TProperty>(Expression<Func<TDerived, TProperty>> propertySelector)
        {
            OnPropertyChanging(WpfUtilities.GeneratePropertyPathString(propertySelector));
            return;
        }

        #endregion

        #region INotifyPropertyChanged implementation

        public event PropertyChangedEventHandler PropertyChanged;

        internal bool TrySendPropertyChangedEvent(string strPropertyName)
        {
            if (PropertyChanged != null)
            {
                var args = EventArgsCache.GetNotifyPropertyChangedEventArgs(strPropertyName);
                PropertyChanged(this, args);
                return true;
            }
            return false;
        }

        protected virtual void OnPropertyChanged(/*[CallerMemberName]*/ string strPropertyName)
        {
            TrySendPropertyChangedEvent(strPropertyName);
            return;
        }

        protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> propertySelector)
        {
            OnPropertyChanged(WpfUtilities.GeneratePropertyPathString(propertySelector));
            return;
        }

        protected void OnPropertyChanged<TDerived, TProperty>(Expression<Func<TDerived, TProperty>> propertySelector)
        {
            OnPropertyChanged(WpfUtilities.GeneratePropertyPathString(propertySelector));
            return;
        }

        #endregion

        [Obsolete]
        public static string GetPropertyName<TProperty>(Expression<Func<TProperty>> propertySelector)
        {
            return WpfUtilities.GeneratePropertyPathString(propertySelector);
        }

        protected Binding GeneratePathBinding<TProperty>(Expression<Func<TProperty>> propertySelector, IValueConverter converter = null)
        {
            string strPath = WpfUtilities.GeneratePropertyPathString(propertySelector);
            var binding = new Binding(strPath)
            {
                Source = this,
            };
            if (converter != null)
                binding.Converter = converter;
            return binding;
        }

        public static PropertyPath GeneratePropertyPath<TProperty>(Expression<Func<TProperty>> propertySelector)
        {
            return new PropertyPath(WpfUtilities.GeneratePropertyPathString(propertySelector));
        }

        protected void AssignProperty<T>(ref T backingValue, T newValue, /*[CallerMemberName]*/ string strPropertyName)
        {
            OnPropertyChanging(strPropertyName);
            backingValue = newValue;
            OnPropertyChanged(strPropertyName);
            return;
        }

        protected void AssignProperty<T>(ref T backingValue, T newValue, Expression<Func<T>> propertySelector)
        {
            AssignProperty(ref backingValue, newValue, WpfUtilities.GeneratePropertyPathString(propertySelector.Body));
            return;
        }

        #region ReportPropertyChange() overrides

        /// <summary>
        /// TODO: Consider caching these inside the viewmodel instance base.
        /// </summary>
        private class DisposePatternOnPropertyChanged : IDisposable
        {
            public ViewModelBase _viewModel = null;
            public string _strPropertyName = null;

            public DisposePatternOnPropertyChanged(ViewModelBase viewModel, string strPropertyName)
            {
                _viewModel = viewModel;
                _strPropertyName = string.Intern(strPropertyName);
                _viewModel.OnPropertyChanging(_strPropertyName);
                return;
            }

            public void Dispose()
            {
                _viewModel.OnPropertyChanged(_strPropertyName);
                return;
            }
        }

        protected IDisposable ReportPropertyChange(/*[CallerMemberName]*/ string strPropertyName)
        {
            return new DisposePatternOnPropertyChanged(this, strPropertyName);
        }

        protected IDisposable ReportPropertyChange<T>(Expression<Func<T>> propertySelector)
        {
            return new DisposePatternOnPropertyChanged(this, WpfUtilities.GeneratePropertyPathString(propertySelector));
        }

        #endregion

        #region SetProperty() overrides

        /// <summary>
        /// A one-statement method of assigning a new property value, handling all databinding events automatically.
        /// </summary>
        /// <returns>true if the underlying value was changed</returns>
        protected bool SetProperty<T>(ref T backingValue, T newValue, /*[CallerMemberName]*/ string strPropertyName)
        {
            /// Deal with it.
            ReadPreamble();

            /// Generic value types aren't allowed to use comparison operators.
            if (!EqualityComparer<T>.Default.Equals(backingValue, newValue))
            {
                AssignProperty(ref backingValue, newValue, strPropertyName);
                return true;
            }
            return false;
        }

        /// <summary>
        /// A one-statement method of assigning a new property value, handling all databinding events automatically.
        /// </summary>
        /// <returns>true if the underlying value was changed</returns>
        /// <param name="property">A lambda expression that only returns a databinding target property.</param>
        protected bool SetProperty<TProperty>(
            ref TProperty backingValue,
            TProperty newValue,
            Expression<Func<TProperty>> propertySelector)
        {
            /// Deal with it.
            ReadPreamble();

            /// Generic value types aren't allowed to use comparison operators.
            if (!EqualityComparer<TProperty>.Default.Equals(backingValue, newValue))
            {
                AssignProperty(ref backingValue, newValue, WpfUtilities.GeneratePropertyPathString(propertySelector));
                return true;
            }
            return false;
        }

        #endregion

        #region Dependency properties

        public static readonly DependencyProperty DialogInvokerProperty = DependencyProperty.Register(
            "DialogInvoker",
            typeof(IDialogInvoker),
            typeof(ViewModelBase),
            new PropertyMetadata(null, DialogInvokerProperty_Changed));

        /// <summary>
        /// </summary>
        public IDialogInvoker DialogInvoker
        {
            get { return (IDialogInvoker)GetValue(DialogInvokerProperty); }
            set { SetValue(DialogInvokerProperty, value); }
        }

        private static void DialogInvokerProperty_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            (d as ViewModelBase).OnDialogInvokerChanged(e.NewValue as IDialogInvoker);
            return;
        }

        protected virtual void OnDialogInvokerChanged(IDialogInvoker dialogInvoker)
        {
            return;
        }

        #endregion
    }

    /********************************************************************************************/
    /// <summary>
    /// </summary>
    /// <typeparam name="TDerived"></typeparam>
    [Serializable]
    public class ViewModelBase<TDerived>
        : ViewModelBase
        where TDerived : class
    {
        /// <summary>
        /// The benefit of a generic class is that each type gets its own copy of static members.
        /// </summary>
        private static Dictionary<string, List<string>> s_propertyDependencyMapping = new Dictionary<string, List<string>>();

        protected static void AddPropertyDependencies(
            string strSourcePropertyName,
            IEnumerable<string> dependentPropertyNames)
        {
            lock (s_propertyDependencyMapping)
            {
                List<string> properties;
                if (!s_propertyDependencyMapping.TryGetValue(strSourcePropertyName, out properties))
                {
                    properties = new List<string>();
                    s_propertyDependencyMapping.Add(strSourcePropertyName, properties);
                }
                properties.AddRange(dependentPropertyNames);
            }
            return;
        }

        protected static void AddPropertyDependencies(
            Expression<Func<TDerived, object>> sourcePropertySelector,
            params Expression<Func<TDerived, object>>[] dependentPropertySelectors)
        {
            AddPropertyDependencies<TDerived>(sourcePropertySelector, dependentPropertySelectors);
            return;
        }

        protected static void AddPropertyDependencies<TViewModel>(
            Expression<Func<TViewModel, object>> sourcePropertySelector,
            params Expression<Func<TViewModel, object>>[] dependentPropertySelectors)
        {
            string strSourcePropertyName = WpfUtilities.GeneratePropertyPathString(sourcePropertySelector);
            AddPropertyDependencies(
                strSourcePropertyName,
                dependentPropertySelectors
                    .Select(e => WpfUtilities.GeneratePropertyPathString(e))
                    .Where(str => str != strSourcePropertyName));
        }

        #region Freezable overrides

        protected override Freezable CreateInstanceCore()
        {
            return new ViewModelBase<TDerived>();
        }

        #endregion

        #region SetProperty() overrides

        /// <summary>
        /// A one-statement method of assigning a new property value, handling all databinding events automatically.
        /// </summary>
        /// <returns>true if the underlying value was changed</returns>
        /// <param name="property">A lambda expression that only returns a databinding target property.</param>
        protected bool SetProperty<TProperty>(
            ref TProperty backingValue,
            TProperty newValue,
            Expression<Func<TDerived, TProperty>> propertySelector)
        {
            /// Deal with it.
            ReadPreamble();

            /// Generic value types aren't allowed to use comparison operators.
            if (!EqualityComparer<TProperty>.Default.Equals(backingValue, newValue))
            {
                AssignProperty(ref backingValue, newValue, WpfUtilities.GeneratePropertyPathString(propertySelector));
                return true;
            }
            return false;
        }

        /// <summary>
        /// C# doesn't have multiple inheritance, so this allows further-derived classes the opportunity
        /// to continue using lambdas to identify properties,
        /// as long as they specify any reference of the desired viewmodel type.
        /// </summary>
        protected bool SetProperty<TViewModel, TProperty>(
            TViewModel viewModelForLambdaTypeInference,
            ref TProperty backingValue,
            TProperty newValue,
            Expression<Func<TViewModel, TProperty>> propertySelector)
        {
            return SetProperty(ref backingValue, newValue, WpfUtilities.GeneratePropertyPathString(propertySelector));
        }

        #endregion

        #region INotifyPropertyChanging implementation

        protected override void OnPropertyChanging(/*[CallerMemberName]*/ string strPropertyName)
        {
            base.OnPropertyChanging(strPropertyName);

            List<string> properties;
            if (s_propertyDependencyMapping.TryGetValue(strPropertyName, out properties))
            {
                for (int iIndex = 0; iIndex < properties.Count; iIndex++)
                    OnPropertyChanging(properties[iIndex]);
            }

            return;
        }

        /// <summary>
        /// C# doesn't have multiple inheritance, so this allows further-derived classes the opportunity
        /// to continue using lambdas to identify properties,
        /// as long as they specify any reference of the desired viewmodel type.
        /// </summary>
        protected void OnPropertyChanging<TViewModel, TProperty>(
            TViewModel viewModelForLambdaTypeInference,
            Expression<Func<TViewModel, TProperty>> propertySelector)
        {
            OnPropertyChanging(WpfUtilities.GeneratePropertyPathString(propertySelector));
            return;
        }

        /// <summary>
        /// This is useful for properties whose actual backing data is stored and manipulated in a different class instance.
        /// </summary>
        public void ExternallyNotifyPropertyChanging(string strPropertyName)
        {
            /// This forces the most-derived override of OnPropertyChanging to get called
            /// as opposed to making OnPropertyChanging public and allowing direct base calls.
            OnPropertyChanging(strPropertyName);
            return;
        }

        /// <summary>
        /// This is useful for properties whose actual backing data is stored and manipulated in a different class instance.
        /// </summary>
        public void ExternallyNotifyPropertyChanging<TProperty>(Expression<Func<TDerived, TProperty>> propertySelector)
        {
            /// This forces the most-derived override of OnPropertyChanging to get called
            /// as opposed to making OnPropertyChanging public and allowing direct base calls.
            OnPropertyChanging(propertySelector);
            return;
        }

        #endregion

        #region INotifyPropertyChanged implementation

        protected override void OnPropertyChanged(/*[CallerMemberName]*/ string strPropertyName)
        {
            base.OnPropertyChanged(strPropertyName);

            List<string> properties;
            if (s_propertyDependencyMapping.TryGetValue(strPropertyName, out properties))
            {
                for (int iIndex = 0; iIndex < properties.Count; iIndex++)
                    OnPropertyChanged(properties[iIndex]);
            }

            return;
        }

        /// <summary>
        /// C# doesn't have multiple inheritance, so this allows further-derived classes the opportunity
        /// to continue using lambdas to identify properties,
        /// as long as they specify any reference of the desired viewmodel type.
        /// </summary>
        protected void OnPropertyChanged<TViewModel, TProperty>(
            TViewModel viewModelForLambdaTypeInference,
            Expression<Func<TViewModel, TProperty>> propertySelector)
        {
            OnPropertyChanged(WpfUtilities.GeneratePropertyPathString(propertySelector));
            return;
        }

        /// <summary>
        /// The ultimate in laziness. Looks up the callstack to identify the property that is being changed.
        /// </summary>
        protected void OnPropertyChanged()
        {
            OnPropertyChanged(GeneralUtilities.GetCallerPropertyName());
            return;
        }

        /// <summary>
        /// This is useful for properties whose actual backing data is stored and manipulated in a different class instance.
        /// </summary>
        public void ExternallyNotifyPropertyChanged(string strPropertyName)
        {
            /// This forces the most-derived override of OnPropertyChanged to get called
            /// as opposed to making OnPropertyChanged public and allowing direct base calls.
            OnPropertyChanged(strPropertyName);
            return;
        }

        /// <summary>
        /// This is useful for properties whose actual backing data is stored and manipulated in a different class instance.
        /// </summary>
        public void ExternallyNotifyPropertyChanged<TProperty>(Expression<Func<TDerived, TProperty>> propertySelector)
        {
            /// This forces the most-derived override of OnPropertyChanged to get called
            /// as opposed to making OnPropertyChanged public and allowing direct base calls.
            OnPropertyChanged(propertySelector);
            return;
        }

        #endregion
    }

    /*public class FreezableViewModel : ViewModelBase
    {
        private bool _bReadOnly = true;
        public bool ReadOnly
        {
            get { return _bReadOnly; }
            protected set { SetProperty(ref _bReadOnly, value, "ReadOnly"); }
        }

        protected FreezableViewModel(bool bReadOnly = false)
        {
            _bReadOnly = bReadOnly;
            return;
        }

        protected override void OnPropertyChanging(string strPropertyName)
        {
            if (_bReadOnly)
                throw new InvalidOperationException("Cannot write to a read-only view model.");

            base.OnPropertyChanging(strPropertyName);
            return;
        }
    }*/
}
